import {sawCollapsedSpans} from "../line/saw_special_spans.js"
import {heightAtLine, visualLineEndNo, visualLineNo} from "../line/spans.js"
import {getLine, lineNumberFor} from "../line/utils_line.js"
import {
    displayHeight,
    displayWidth,
    getDimensions,
    paddingVert,
    scrollGap
} from "../measurement/position_measurement.js"
import {mac, webkit} from "../util/browser.js"
import {activeElt, removeChildren, contains} from "../util/dom.js"
import {hasHandler, signal} from "../util/event.js"
import {indexOf} from "../util/misc.js"

import {buildLineElement, updateLineForChanges} from "./update_line.js"
import {startWorker} from "./highlight_worker.js"
import {maybeUpdateLineNumberWidth} from "./line_numbers.js"
import {measureForScrollbars, updateScrollbars} from "./scrollbars.js"
import {updateSelection} from "./selection.js"
import {updateHeightsInViewport, visibleLines} from "./update_lines.js"
import {adjustView, countDirtyView, resetView} from "./view_tracking.js"

// DISPLAY DRAWING

export class DisplayUpdate {
    constructor(cm, viewport, force) {
        let display = cm.display

        this.viewport = viewport
        // Store some values that we'll need later (but don't want to force a relayout for)
        this.visible = visibleLines(display, cm.doc, viewport)
        this.editorIsHidden = !display.wrapper.offsetWidth
        this.wrapperHeight = display.wrapper.clientHeight
        this.wrapperWidth = display.wrapper.clientWidth
        this.oldDisplayWidth = displayWidth(cm)
        this.force = force
        this.dims = getDimensions(cm)
        this.events = []
    }

    signal(emitter, type) {
        if (hasHandler(emitter, type))
            this.events.push(arguments)
    }

    finish() {
        for (let i = 0; i < this.events.length; i++)
            signal.apply(null, this.events[i])
    }
}

export function maybeClipScrollbars(cm) {
    let display = cm.display
    if (!display.scrollbarsClipped && display.scroller.offsetWidth) {
        display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth
        display.heightForcer.style.height = scrollGap(cm) + "px"
        display.sizer.style.marginBottom = -display.nativeBarWidth + "px"
        display.sizer.style.borderRightWidth = scrollGap(cm) + "px"
        display.scrollbarsClipped = true
    }
}

function selectionSnapshot(cm) {
    if (cm.hasFocus()) return null
    let active = activeElt()
    if (!active || !contains(cm.display.lineDiv, active)) return null
    let result = {activeElt: active}
    if (window.getSelection) {
        let sel = window.getSelection()
        if (sel.anchorNode && sel.extend && contains(cm.display.lineDiv, sel.anchorNode)) {
            result.anchorNode = sel.anchorNode
            result.anchorOffset = sel.anchorOffset
            result.focusNode = sel.focusNode
            result.focusOffset = sel.focusOffset
        }
    }
    return result
}

function restoreSelection(snapshot) {
    if (!snapshot || !snapshot.activeElt || snapshot.activeElt == activeElt()) return
    snapshot.activeElt.focus()
    if (snapshot.anchorNode && contains(document.body, snapshot.anchorNode) && contains(document.body, snapshot.focusNode)) {
        let sel = window.getSelection(), range = document.createRange()
        range.setEnd(snapshot.anchorNode, snapshot.anchorOffset)
        range.collapse(false)
        sel.removeAllRanges()
        sel.addRange(range)
        sel.extend(snapshot.focusNode, snapshot.focusOffset)
    }
}

// Does the actual updating of the line display. Bails out
// (returning false) when there is nothing to be done and forced is
// false.
export function updateDisplayIfNeeded(cm, update) {
    let display = cm.display, doc = cm.doc

    if (update.editorIsHidden) {
        resetView(cm)
        return false
    }

    // Bail out if the visible area is already rendered and nothing changed.
    if (!update.force &&
        update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo &&
        (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) &&
        display.renderedView == display.view && countDirtyView(cm) == 0)
        return false

    if (maybeUpdateLineNumberWidth(cm)) {
        resetView(cm)
        update.dims = getDimensions(cm)
    }

    // Compute a suitable new viewport (from & to)
    let end = doc.first + doc.size
    let from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first)
    let to = Math.min(end, update.visible.to + cm.options.viewportMargin)
    if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom)
    if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo)
    if (sawCollapsedSpans) {
        from = visualLineNo(cm.doc, from)
        to = visualLineEndNo(cm.doc, to)
    }

    let different = from != display.viewFrom || to != display.viewTo ||
        display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth
    adjustView(cm, from, to)

    display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom))
    // Position the mover div to align with the current scroll position
    cm.display.mover.style.top = display.viewOffset + "px"

    let toUpdate = countDirtyView(cm)
    if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view &&
        (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo))
        return false

    // For big changes, we hide the enclosing element during the
    // update, since that speeds up the operations on most browsers.
    let selSnapshot = selectionSnapshot(cm)
    if (toUpdate > 4) display.lineDiv.style.display = "none"
    patchDisplay(cm, display.updateLineNumbers, update.dims)
    if (toUpdate > 4) display.lineDiv.style.display = ""
    display.renderedView = display.view
    // There might have been a widget with a focused element that got
    // hidden or updated, if so re-focus it.
    restoreSelection(selSnapshot)

    // Prevent selection and cursors from interfering with the scroll
    // width and height.
    removeChildren(display.cursorDiv)
    removeChildren(display.selectionDiv)
    display.gutters.style.height = display.sizer.style.minHeight = 0

    if (different) {
        display.lastWrapHeight = update.wrapperHeight
        display.lastWrapWidth = update.wrapperWidth
        startWorker(cm, 400)
    }

    display.updateLineNumbers = null

    return true
}

export function postUpdateDisplay(cm, update) {
    let viewport = update.viewport

    for (let first = true; ; first = false) {
        if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) {
            // Clip forced viewport to actual scrollable area.
            if (viewport && viewport.top != null)
                viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}
            // Updated line heights might result in the drawn area not
            // actually covering the viewport. Keep looping until it does.
            update.visible = visibleLines(cm.display, cm.doc, viewport)
            if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo)
                break
        }
        if (!updateDisplayIfNeeded(cm, update)) break
        updateHeightsInViewport(cm)
        let barMeasure = measureForScrollbars(cm)
        updateSelection(cm)
        updateScrollbars(cm, barMeasure)
        setDocumentHeight(cm, barMeasure)
        update.force = false
    }

    update.signal(cm, "update", cm)
    if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) {
        update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo)
        cm.display.reportedViewFrom = cm.display.viewFrom;
        cm.display.reportedViewTo = cm.display.viewTo
    }
}

export function updateDisplaySimple(cm, viewport) {
    let update = new DisplayUpdate(cm, viewport)
    if (updateDisplayIfNeeded(cm, update)) {
        updateHeightsInViewport(cm)
        postUpdateDisplay(cm, update)
        let barMeasure = measureForScrollbars(cm)
        updateSelection(cm)
        updateScrollbars(cm, barMeasure)
        setDocumentHeight(cm, barMeasure)
        update.finish()
    }
}

// Sync the actual display DOM structure with display.view, removing
// nodes for lines that are no longer in view, and creating the ones
// that are not there yet, and updating the ones that are out of
// date.
function patchDisplay(cm, updateNumbersFrom, dims) {
    let display = cm.display, lineNumbers = cm.options.lineNumbers
    let container = display.lineDiv, cur = container.firstChild

    function rm(node) {
        let next = node.nextSibling
        // Works around a throw-scroll bug in OS X Webkit
        if (webkit && mac && cm.display.currentWheelTarget == node)
            node.style.display = "none"
        else
            node.parentNode.removeChild(node)
        return next
    }

    let view = display.view, lineN = display.viewFrom
    // Loop over the elements in the view, syncing cur (the DOM nodes
    // in display.lineDiv) with the view as we go.
    for (let i = 0; i < view.length; i++) {
        let lineView = view[i]
        if (lineView.hidden) {
        } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet
            let node = buildLineElement(cm, lineView, lineN, dims)
            container.insertBefore(node, cur)
        } else { // Already drawn
            while (cur != lineView.node) cur = rm(cur)
            let updateNumber = lineNumbers && updateNumbersFrom != null &&
                updateNumbersFrom <= lineN && lineView.lineNumber
            if (lineView.changes) {
                if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false
                updateLineForChanges(cm, lineView, lineN, dims)
            }
            if (updateNumber) {
                removeChildren(lineView.lineNumber)
                lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN)))
            }
            cur = lineView.node.nextSibling
        }
        lineN += lineView.size
    }
    while (cur) cur = rm(cur)
}

export function updateGutterSpace(display) {
    let width = display.gutters.offsetWidth
    display.sizer.style.marginLeft = width + "px"
}

export function setDocumentHeight(cm, measure) {
    cm.display.sizer.style.minHeight = measure.docHeight + "px"
    cm.display.heightForcer.style.top = measure.docHeight + "px"
    cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px"
}
